package blockchain

import (
	"fmt"
	"crypto/sha256"
	"math/big"
	"math"
	"encoding/json"
	"encoding/binary"
	"encoding/hex"
	"bytes"
)

const (
	DEBUG = true
	DIFFICULTY = 15
	TXS_LIMIT = 2
)

type BlockChain []*Block

type Block struct {
	Transactions []Transaction
	Difficulty uint8
	// Mapping map[sting]uint64
	PrevHash []byte // hash(B[i-1])
	CurrHash []byte // hash(Data+PrevHash)
	Nonce uint64 // pow(CurrHash)
}

func (block *Block) AddTransaction(chain *BlockChain, tx *Transaction) error {
	if tx == nil {
		return errors.New("tx is null")
	}
	if tx.Value == 0 {
		return errors.New("tx value = 0")
	}
	// if tx.Sender != STORAGE_CHAIN && len(block.Transactions) == TXS_LIMIT {
	if len(block.Transactions) == TXS_LIMIT {
		return errors.New("len tx = limit")
	}
	// if tx.Sender != STORAGE_CHAIN && tx.Value > START_PERCENT && tx.ToStorage != STORAGE_REWARD {
	// 	return errors.New("storage reward pass")
	// }
	// if !bytes.Equal(tx.PrevBlock, chain.LastHash()) {
	// 	return errors.New("prev block in tx /= last hash in chain")
	// }
	// var balanceInChain uint64
	// balanceInTX := tx.Value  //+ tx.ToStorage
	// if value, ok := block.Mapping[tx.Sender]; ok {
	// 	balanceInChain = value
	// } else {
	// 	balanceInChain = chain.Balance(tx.Sender, chain.Size())
	// }
	// if balanceInTX > balanceInChain {
	// 	return errors.New("insufficient funds")
	// }
	// block.Mapping[tx.Sender] = balanceInChain - balanceInTX
	// block.addBalance(chain, tx.Receiver, tx.Value)
	// block.addBalance(chain, STORAGE_CHAIN, tx.ToStorage)
	block.Transactions = append(block.Transactions, *tx)
	return nil
}

type Transaction struct {
	// RandBytes []byte
	// PrevBlock []byte
	Sender string
	Receiver string
	Value uint64
	// ToStorage uint64
	// CurrHash []byte
	// Signature []byte
}

func NewTransaction(user *User, to string, value uint64) *Transaction {
	tx := &Transaction{
		//RandBytes: GenerateRandomBytes(RAND_BYTES),
		//PrevBlock: lasthash,
		Sender: user.Address(),
		Receiver: to,
		Value: value,
	}
	// if value > START_PERCENT {
	// 	tx.ToStorage = STORAGE_REWARD
	// }
	// tx.CurrHash = tx.hash()
	// tx.Signature = tx.sign(user.Private())
	return tx
}

type User struct {
	PrivateKey *rsa.PrivateKey
} 

func (user *User) Address() string {
	return StringPublic(user.Public())
} 

func (user *User) Public() *rsa.PublicKey {
	return &(user.PrivateKey).PublicKey
} 

func (user *User) Private() *rsa.PrivateKey {
	return user.PrivateKey
} 

func NewChain() BlockChain {
	return BlockChain{
		NewBlock("First Block", []byte("GENESIS")),
	}
}

func NewBlock(transactions *Transaction, prevHash []byte) *Block {
	block := &Block{
		Transactions: transactions,
		Difficulty: DIFFICULTY,
		PrevHash: prevHash,
	}
	block.CurrHash = HashSum(bytes.Join(
		[][]byte{
			[]byte(block.Data),
			ToBytes(uint64(block.Difficulty)),
			block.PrevHash,
		},
		[]byte{},
	))
	block.Nonce = ProofOfWork(block.CurrHash, block.Difficulty)
	return block
}

func HashSum(data []byte) []byte {
	hash := sha256.Sum256(data)
	return hash[:]
}

func ProofOfWork(blockHash []byte, diff uint8) uint64 {
	var (
		Target  = big.NewInt(1)
		intHash = big.NewInt(1)
		nonce   = uint64(0)
		hash    []byte
	)
	Target.Lsh(Target, 256-uint(diff))
	for nonce < math.MaxUint64 {
		hash = HashSum(bytes.Join(
			[][]byte{
				blockHash,
				ToBytes(nonce),
			},
			[]byte{},
		))
		if DEBUG {
			fmt.Printf("\rMining: %s", hex.EncodeToString(hash))
		}
		intHash.SetBytes(hash)
		if intHash.Cmp(Target) == -1 {
			if DEBUG {
				fmt.Println()
			}
			return nonce
		}
		nonce++
	}
	return nonce
}

func ToBytes(num uint64) []byte {
	var data = new(bytes.Buffer)
	err := binary.Write(data, binary.BigEndian, num)
	if err != nil {
		return nil
	}
	return data.Bytes()
}

func SerializeBlock(block *Block) string {
	jsonData, err := json.MarshalIndent(*block, "", "\t")
	if err != nil {
		return ""
	}
	return string(jsonData)
}

func DeserializeBlock(data string) *Block {
	var block = new(Block)
	err := json.Unmarshal([]byte(data), block)
	if err != nil {
		return nil
	}
	return block
}
